package main;

import java.io.IOException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

//Nikolas Wolfe
//4/13/09
//CIS 4930
//Game of Life 2

/* =============================================================================
* The Game of Life
*/

public class GameOfLife
{
	// thread IDs
	private static final AtomicInteger uniqueID =
		new AtomicInteger(1);
	
	// color constants
	static private final int WHITE 	= 0xFFFFFFFF;
	static private final int RED 	= 0xFFFF0000; 
	static private final int GREEN 	= 0xFF00FF00;
	static private final int BLUE 	= 0xFF0000FF;
	
	// size constraints
	private final int WIDTH;
	private final int HEIGHT;
	private int NUM_THREADS;
	private int NUM_GENERATIONS;
	
	// threading services
	private ExecutorService exec;
	private CyclicBarrier barrier;
	private Worker[] workers;
	
	// resources
	private ToroidalArray<Integer> currentGenArray;
	private ToroidalArray<Integer> workAreaArray;
	private ConfigurationManager configManager;
	private BarrierStatistics barrierStats;
	private StatService statService;
	
	// Generation count
	private int generationCount;
	
	// Total Simulation Time Variables
	private long startTime;
	private long stopTime;
	
	/* =========================================================================
	 * Constructor
	 */ 
	public GameOfLife(int numThreads, int numGenerations,  ConfigurationManager configManager )
	{
		System.out.println();
		System.out.println("------------------------------------------------------------");
		System.out.println("================== Conway's Game Of Life ===================");
		System.out.println("------------------------------------------------------------");
		System.out.println();
		
		// parameters
		this.configManager = configManager;
		this.WIDTH = configManager.getWIDTH();
		this.HEIGHT = configManager.getHEIGHT();
		this.NUM_GENERATIONS = numGenerations;
		this.NUM_THREADS = numThreads;
		
		// src / dest arrays
		this.currentGenArray = new ToroidalArray<Integer>(WIDTH, HEIGHT);
		this.workAreaArray = new ToroidalArray<Integer>(WIDTH, HEIGHT);
		this.barrierStats = new BarrierStatistics();
		this.statService = new StatService();
		
		// initialize array
		currentGenArray = configManager.getConfiguration();
		
		// worker thread array
		this.workers = new Worker[NUM_THREADS];
		
		// time values
		this.startTime = 0;
		this.stopTime = 0;
		
		// init generation count
		clearGenerationCount();
		
		// initialize the barrier
		initialize();
	}
	
	/* =========================================================================
	 * Initialize barrier and execution service
	 */ 
	private void initialize()
	{
		this.workers = parcelThreadWork( workers );
		this.exec = Executors.newFixedThreadPool( NUM_THREADS );
		this.barrier = new CyclicBarrier( NUM_THREADS,
			new Runnable() 
			{
				public void run()
				{
					// update our timer
					barrierStats.registerGenerationStopTimeStamp( getGenerationCount(), System.nanoTime() );
					barrierStats.calcElapsedTime( getGenerationCount() );
					
					// we've elapsed one generation, switch src / destination
					ToroidalArray<Integer> temp = currentGenArray;
					currentGenArray = workAreaArray;
					workAreaArray = temp;
					
					// register statistics and updates
					for( Worker i : workers )
					{
						statService.register( i.getThreadStats().getCurrentReport() );
					}
					
					// register generation info
					statService.register( barrierStats.getCurrentReport() );
					barrierStats.flushSummary();
					
					// start stat report for next generation
					if( getGenerationCount() < NUM_GENERATIONS )
					{
						incGenerationCount();
						barrierStats.registerGenerationStartTimeStamp( getGenerationCount(), System.nanoTime());
						statService.register(barrierStats.getCurrentReport());
						barrierStats.flushSummary();
					} 
				} 
			} 
			
		);
		System.out.println("******************** Simulation Summary ********************\n");
		System.out.println("Number of Threads: " + NUM_THREADS );
		System.out.println("Number of Generations: " + NUM_GENERATIONS );
		System.out.println("Grid Size: " + WIDTH + " X " + HEIGHT + " cells\n" );
	}
	
	/* =========================================================================
	 * Output final status
	 */ 
	private void outputFinalStatus()
	{
		try
		{
			configManager.outputFinalConfiguration( currentGenArray, 
					NUM_THREADS, NUM_GENERATIONS, preparePrintReport() );
		} 
		catch (IOException e)
		{
			e.printStackTrace();
		}
	}
	
	/* =========================================================================
	 * Print the Status of this simulation
	 */ 
	private StatService preparePrintReport()
	{
		String overallTimeMessage = "Total Simulation Time: " + 
			getOverallSimulationTime() + " us\n";
		statService.register( overallTimeMessage );
		
		String outputSummaryMessage 	= "Simulation Averages Summary:\n";
		outputSummaryMessage 		   += "----------------------------\n";
		statService.register( outputSummaryMessage );
		
		// add / print averages
		for( Worker i : workers )
		{
			// The last thing these threads did was calculate averages
			statService.register( i.getThreadStats().getCurrentReport() );
		}
		barrierStats.calculateAverageTime();
		statService.register( barrierStats.getCurrentReport() );
		return statService;
	}
	
	/* =========================================================================
	 * Start the threads
	 */ 
	public void runCyclicBarrier() throws InterruptedException 
	{
		// check that shit's up to snuff
		if( exec == null || barrier == null )
			throw new IllegalStateException( "Barrier not initialized!" );
		
		// collect initial barrier stats
		barrierStats.registerGenerationStartTimeStamp( getGenerationCount(), System.nanoTime() ); // start the barrier clock
		statService.register( barrierStats.getCurrentReport() );
		barrierStats.flushSummary();
		
		// start them threads
		for( Worker w : workers )
		{
			exec.execute( w );
		}
		setStartTime();
	}
	
	/* =========================================================================
	 * Stop the threads -- kill the motherfuckers!
	 */ 
	public void forceQuitCyclicBarrier()
	{
		exec.shutdownNow();
	}
	
	/* =========================================================================
	 * Stop the threads -- gracefully, from within
	 */ 
	private void stopCyclicBarrier()
	{
		exec.shutdown();
		for( Worker i : workers )
		{
			try
			{
				i.join();
			} 
			catch (InterruptedException e)
			{
				Thread.currentThread().interrupt();
			}
		}
	}
	
	/* =========================================================================
	 * Parcel the grid for individual threads to do their work
	 */ 
	private Worker[] parcelThreadWork( Worker[] workers )
	{
		// vertically divide the grid
		int chunkSize = ( HEIGHT / NUM_THREADS ); 
		int startIndex = 0; 
		int stopIndex = startIndex + chunkSize;
		
		// hand each worker their assigned share for this simulation
		for( int i = 0; i < workers.length; ++i )
		{
			if( stopIndex > HEIGHT-1 )
			{
				stopIndex = HEIGHT-1;
			}
			workers[i] = new Worker( startIndex, stopIndex, NUM_GENERATIONS );
			startIndex = stopIndex + 1;
			stopIndex = startIndex + chunkSize;
		}
		return workers;
	}
	
	/* =========================================================================
	 * Algorithm for subsection traversal
	 */ 
	private void traverseSection( ToroidalArray<Integer> arr, int startIndex, int stopIndex )
	{
		for( int i = startIndex; i <= stopIndex; ++i ) // do the specified rows
		{
			for( int j = 0; j < arr.getYMax(); ++j )
			{
				// determine this cell's liveness, number of live neighbor cells
				boolean isAlive = ( arr.get(i,j) == GREEN || arr.get(i,j) == BLUE );
				int liveNeighborCount = getLiveNeighborCount( arr, i, j );
				
				// die of loneliness
				if( isAlive && liveNeighborCount < 2 )
				{
					// RED = died this generation
					workAreaArray.set(i, j, RED); 
				}
				
				// die of overcrowding
				else if( isAlive && liveNeighborCount > 3 )
				{
					// RED = died this generation
					workAreaArray.set(i, j, RED);
				}
				
				// cell is alive and green this generation
				else if( isAlive && arr.get(i,j) == GREEN )
				{
					// BLUE = live for more than 1 generation
					workAreaArray.set(i, j, BLUE);
				}
				
				// spring to life!
				else if( !isAlive && liveNeighborCount == 3 )
				{
					// GREEN = born this generation
					workAreaArray.set(i, j, GREEN);
				}
				
				// cell is dead and red this generation
				else if( !isAlive && arr.get(i,j) == RED )
				{
					// WHITE = dead for more than 1 generation
					workAreaArray.set(i, j, WHITE);
				}
				
				else // stay the same
				{
					workAreaArray.set(i, j, arr.get(i, j));
				}
			}
		}
	}
	
	/* =========================================================================
	 * Determine the number of adjacent live cells
	 */ 
	private int getLiveNeighborCount( ToroidalArray<Integer> arr, int x, int y)
	{
		int liveCount = 0;
		
		// figure out the adjaceny of live cells surrounding this point
		liveCount += inspectNeighbor( arr, (x-1), (y) ); // North
		liveCount += inspectNeighbor( arr, (x+1), (y) ); // South
		liveCount += inspectNeighbor( arr, (x), (y+1) ); // East
		liveCount += inspectNeighbor( arr, (x), (y-1) ); // West
		
		liveCount += inspectNeighbor( arr, (x-1), (y-1) ); // Northwest
		liveCount += inspectNeighbor( arr, (x-1), (y+1) ); // Northeast
		liveCount += inspectNeighbor( arr, (x+1), (y-1) ); // Southwest
		liveCount += inspectNeighbor( arr, (x+1), (y+1) ); // Southeast
		
		return liveCount;
	}
	
	/* =========================================================================
	 * Determine the liveness of an adjacent cell -- return 1 if live, 0 if dead
	 */ 
	private int inspectNeighbor( ToroidalArray<Integer> arr, int x, int y )
	{
		if( arr.get(x,y) == GREEN || arr.get(x,y) == BLUE )
		{
			return 1;
		}
		else {
			return 0;
		}
	}

	/* =========================================================================
	 * Increment the generation counter
	 */
	private void incGenerationCount()
	{
		++generationCount;
	}
	
	/* =========================================================================
	 * Get the generation counter
	 */
	private int getGenerationCount()
	{
		return generationCount;
	}
	
	/* =========================================================================
	 * Reset the generation counter
	 */
	private void clearGenerationCount()
	{
		this.generationCount = 1;
	}
	
	/* =========================================================================
	 * Total Simulation Timer
	 */
	private long getSimulationTime()
	{
		return System.nanoTime();
	}
	private void setStartTime()
	{
		this.startTime = getSimulationTime();
	}
	private void setStopTime()
	{
		this.stopTime = getSimulationTime();
	}
	
	/* =========================================================================
	 * Final Overall Simulation Duration
	 */
	public double getOverallSimulationTime()
	{
		if( stopTime == 0 )
			throw new IllegalStateException( "Simulation not started!" );
		
		return (double)Math.abs( stopTime - startTime );
	}
	
	/* =========================================================================
	 * Worker class -- Defines the behavior of a thread doing updates on grid
	 */  
	private class Worker extends Thread
	{
		private int startIndex;
		private int stopIndex;
		private int numIterations;
		private int id;
		private ThreadStatistics stats;
		
		public Worker(int start, int stop, int numIter )
		{
			this.startIndex = start;
			this.stopIndex = stop;
			this.numIterations = numIter;
			this.id = uniqueID.getAndIncrement();
			this.stats = new ThreadStatistics();
		}
		
		public void run()
		{
			while( !Thread.interrupted() && !doneWorking() )
			{
				// flush stats
				stats.flushSummary();
				try 
				{
					// do our part!
					stats.registerThreadStartTime( System.nanoTime() );
					traverseSection( currentGenArray, startIndex, stopIndex );
					stats.registerThreadStopTime( id , System.nanoTime() );
					
					// wait on the barrier
					barrier.await(); 
				}
				catch ( InterruptedException e )
				{
					Thread.currentThread().interrupt();
				}
				catch ( BrokenBarrierException e )
				{
					return;
				}
			}
			
			// calc average execution time
			stats.flushSummary();
			stats.calculateAverageTime( id );
			
			if( this.id == 1 ) // only 1 thread should do this
			{
				// kill the cyclic barrier, output status
				setStopTime();
				stopCyclicBarrier();
				outputFinalStatus();
			}
		}
		private boolean doneWorking()
		{
			return (numIterations--) <= 0 ;
		}
		public Statistics getThreadStats()
		{
			return stats;
		}
	}
}
